/*
 * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.greenrobot.greendao;

import ohos.data.rdb.RdbStore;
import ohos.data.rdb.Statement;
import ohos.data.resultset.ResultSet;
import ohos.data.resultset.SharedBlock;
import ohos.data.resultset.SharedResultSet;
import rx.schedulers.Schedulers;

import org.greenrobot.greendao.annotation.apihint.Experimental;
import org.greenrobot.greendao.database.Database;
import org.greenrobot.greendao.database.DatabaseStatement;
import org.greenrobot.greendao.identityscope.IdentityScope;
import org.greenrobot.greendao.identityscope.IdentityScopeLong;
import org.greenrobot.greendao.internal.DaoConfig;
import org.greenrobot.greendao.internal.FastCursor;
import org.greenrobot.greendao.internal.TableStatements;
import org.greenrobot.greendao.query.Query;
import org.greenrobot.greendao.query.QueryBuilder;
import org.greenrobot.greendao.rx.RxDao;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

/**
 * Base class for all DAOs: Implements entity operations like insert, load, delete, and query.
 * <p>
 * This class is thread-safe.
 *  When operating on TX, statements, or identity scope the following locking order must be met to avoid deadlocks:
 *
 *  1.) If not inside a TX already, begin a TX to acquire a DB connection (connection is to be handled like a lock)
 *  2.) The DatabaseStatement
 *  3.) identityScope
 *
 * @param <T> Entity type
 * @param <K> Primary key (PK) type; use Void if entity does not have exactly one PK
 * @author Markus
 */
public abstract class AbstractDao<T, K> {
    /**
     * Daoconfig element
     */
    protected final DaoConfig config;

    /**
     * Database variale
     */
    protected final Database db;

    /**
     * Check if it is standard SQLite
     */
    protected boolean isStandardSQLite;

    /**
     * Identity scope key value pair
     */
    protected final IdentityScope<K, T> identityScope;

    /**
     * Identity scope for long keys
     */
    protected final IdentityScopeLong<T> identityScopeLong;

    /**
     * Table Statements to be executed
     */
    protected final TableStatements statements;

    /**
     * Dao session for transaction
     */
    protected final AbstractDaoSession session;

    /**
     * Primary key ordinal
     */
    protected final int pkOrdinal;

    private volatile RxDao<T, K> rxDao;

    private volatile RxDao<T, K> rxDaoPlain;

    /**
     * Abstract dao constructor
     * @param config daoconfig
     */
    public AbstractDao(DaoConfig config) {
        this(config, null);
    }

    @SuppressWarnings("unchecked")
    /**
     * Abstract dao constructor with config and session
     */
    public AbstractDao(DaoConfig config, AbstractDaoSession daoSession) {
        this.config = config;
        this.session = daoSession;
        db = config.db;
        isStandardSQLite = db.getRawDatabase() instanceof RdbStore;
        identityScope = (IdentityScope<K, T>) config.getIdentityScope();
        if (identityScope instanceof IdentityScopeLong) {
            identityScopeLong = (IdentityScopeLong<T>) identityScope;
        } else {
            identityScopeLong = null;
        }
        statements = config.statements;
        pkOrdinal = config.pkProperty != null ? config.pkProperty.ordinal : -1;
    }

    /**
     * Get abstract dao session
     * @return abstract dao session
     */
    public AbstractDaoSession getSession() {
        return session;
    }

    /**
     * Get statements for transaction
     * @return TableStatements
     */
    TableStatements getStatements() {
        return config.statements;
    }

    /**
     * Get Config table name
     * @return table name
     */
    public String getTablename() {
        return config.tablename;
    }

    /**
     * Get the property list of config
     * @return property list
     */
    public Property[] getProperties() {
        return config.properties;
    }

    /**
     * Get the Primary key property
     * @return property
     */
    public Property getPkProperty() {
        return config.pkProperty;
    }

    /**
     * Get all columns of the config
     * @return column list
     */
    public String[] getAllColumns() {
        return config.allColumns;
    }

    /**
     * get the Primary key columns
     * @return column list
     */
    public String[] getPkColumns() {
        return config.pkColumns;
    }

    /**
     * get the non primary key columns
     * @return columns list
     */
    public String[] getNonPkColumns() {
        return config.nonPkColumns;
    }

    /**
     * Loads the entity for the given PK.
     *
     * @param key a PK value or null
     * @return The entity or null, if no entity matched the PK value
     */
    public T load(K key) {
        assertSinglePk();
        if (key == null) {
            return null;
        }
        if (identityScope != null) {
            T entity = identityScope.get(key);
            if (entity != null) {
                return entity;
            }
        }
        String sql = statements.getSelectByKey();
        String[] keyArray = new String[] {key.toString()};
        ResultSet cursor = db.rawQuery(sql, keyArray);
        return loadUniqueAndCloseCursor(cursor);
    }

    /**
     * Load by row id
     * @param rowId row id
     * @return entity data
     */
    public T loadByRowId(long rowId) {
        String[] idArray = new String[] {Long.toString(rowId)};
        ResultSet resultSet = db.rawQuery(statements.getSelectByRowId(), idArray);
        return loadUniqueAndCloseCursor(resultSet);
    }

    /**
     * Load unique and close cursor
     * @param cursor cursor to be loaded
     * @return entity data
     */
    protected T loadUniqueAndCloseCursor(ResultSet cursor) {
        try {
            return loadUnique(cursor);
        } finally {
            cursor.close();
        }
    }

    /**
     * Load unique result
     * @param cursor cursor to be loaded
     * @return entity data
     */
    protected T loadUnique(ResultSet cursor) {
        boolean available = cursor.goToFirstRow();
        if (!available) {
            return null;
        } else if (!cursor.isAtLastRow()) {
            throw new DaoException("Expected unique result, but count was " + cursor.getRowCount());
        }
        return loadCurrent(cursor, 0, true);
    }

    /**
     * Loads all available entities from the database.
     * @return list of all available entities
     */
    public List<T> loadAll() {
        ResultSet resultset = db.rawQuery(statements.getSelectAll(), null);
        return loadAllAndCloseCursor(resultset);
    }

    /**
     * Detaches an entity from the identity scope (session). Subsequent query results won't return this object.
     * @param entity entity to be detached
     * @return whether entity detached or not
     */
    public boolean detach(T entity) {
        if (identityScope != null) {
            K key = getKeyVerified(entity);
            return identityScope.detach(key, entity);
        } else {
            return false;
        }
    }

    /**
     * Detaches all entities (of type T) from the identity scope (session).
     * Subsequent query results won't return any previously loaded objects.
     */
    public void detachAll() {
        if (identityScope != null) {
            identityScope.clear();
        }
    }

    /**
     * load all and close cursor
     * @param cursor cursor to be loaded
     * @return list of entity
     */
    protected List<T> loadAllAndCloseCursor(ResultSet cursor) {
        try {
            return loadAllFromCursor(cursor);
        } finally {
            cursor.close();
        }
    }

    /**
     * Inserts the given entities in the database using a transaction.
     *
     * @param entities The entities to insert.
     */
    public void insertInTx(Iterable<T> entities) {
        insertInTx(entities, isEntityUpdateable());
    }

    /**
     * Inserts the given entities in the database using a transaction.
     *
     * @param entities The entities to insert.
     */
    public void insertInTx(T... entities) {
        insertInTx(Arrays.asList(entities), isEntityUpdateable());
    }

    /**
     * Inserts the given entities in the database using a transaction. The given entities will become tracked if the PK
     * is set.
     *
     * @param entities The entities to insert.
     * @param setPrimaryKey if true, the PKs of the given will be set after the insert; pass false to improve
     * performance.
     */
    public void insertInTx(Iterable<T> entities, boolean setPrimaryKey) {
        executeInsertInTx(statements.getInsertStatement(), entities, setPrimaryKey);
    }

    /**
     * Inserts or replaces the given entities in the database using a transaction. The given entities will become
     * tracked if the PK is set.
     *
     * @param entities The entities to insert.
     * @param setPrimaryKey if true, the PKs of the given will be set after the insert; pass false to improve
     * performance.
     */
    public void insertOrReplaceInTx(Iterable<T> entities, boolean setPrimaryKey) {
        executeInsertInTx(statements.getInsertOrReplaceStatement(), entities, setPrimaryKey);
    }

    /**
     * Inserts or replaces the given entities in the database using a transaction.
     *
     * @param entities The entities to insert.
     */
    public void insertOrReplaceInTx(Iterable<T> entities) {
        insertOrReplaceInTx(entities, isEntityUpdateable());
    }

    /**
     * Inserts or replaces the given entities in the database using a transaction.
     *
     * @param entities The entities to insert.
     */
    public void insertOrReplaceInTx(T... entities) {
        insertOrReplaceInTx(Arrays.asList(entities), isEntityUpdateable());
    }

    private void executeInsertInTx(DatabaseStatement stmt, Iterable<T> entities, boolean setPrimaryKey) {
        db.beginTransaction();
        try {
            synchronized (stmt) {
                if (identityScope != null) {
                    identityScope.lock();
                }
                try {
                    if (isStandardSQLite) {
                        Statement rawStmt = (Statement) stmt.getRawStatement();
                        for (T entity : entities) {
                            bindValues(rawStmt, entity);
                            if (setPrimaryKey) {
                                long rowId = rawStmt.executeAndGetLastInsertRowId();
                                updateKeyAfterInsertAndAttach(entity, rowId, false);
                            } else {
                                rawStmt.execute();
                            }
                        }
                    } else {
                        for (T entity : entities) {
                            bindValues(stmt, entity);
                            if (setPrimaryKey) {
                                long rowId = stmt.executeInsert();
                                updateKeyAfterInsertAndAttach(entity, rowId, false);
                            } else {
                                stmt.execute();
                            }
                        }
                    }
                } finally {
                    if (identityScope != null) {
                        identityScope.unlock();
                    }
                }
            }
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }
    }

    /**
     * Insert an entity into the table associated with a concrete DAO.
     *
     * @param entity the entity to insert
     * @return row ID of newly inserted entity
     */
    public long insert(T entity) {
        return executeInsert(entity, statements.getInsertStatement(), true);
    }

    /**
     * Insert an entity into the table associated with a concrete DAO <b>without</b> setting key property.
     * <p>
     * Warning: This may be faster, but the entity should not be used anymore. The entity also won't be attached to
     * identity scope.
     *
     * @param entity entity to insert
     * @return row ID of newly inserted entity
     */
    public long insertWithoutSettingPk(T entity) {
        return executeInsert(entity, statements.getInsertOrReplaceStatement(), false);
    }

    /**
     * Insert an entity into the table associated with a concrete DAO.
     *
     * @param entity entity to insert
     * @return row ID of newly inserted entity
     */
    public long insertOrReplace(T entity) {
        return executeInsert(entity, statements.getInsertOrReplaceStatement(), true);
    }

    private long executeInsert(T entity, DatabaseStatement stmt, boolean setKeyAndAttach) {
        long rowId;
        if (db.isDbLockedByCurrentThread()) {
            rowId = insertInsideTx(entity, stmt);
        } else {
            // Do TX to acquire a connection before locking the stmt to avoid deadlocks
            db.beginTransaction();
            try {
                rowId = insertInsideTx(entity, stmt);
                db.setTransactionSuccessful();
            } finally {
                db.endTransaction();
            }
        }
        if (setKeyAndAttach) {
            updateKeyAfterInsertAndAttach(entity, rowId, true);
        }
        return rowId;
    }

    private long insertInsideTx(T entity, DatabaseStatement stmt) {
        synchronized (stmt) {
            if (isStandardSQLite) {
                Statement rawStmt = (Statement) stmt.getRawStatement();
                bindValues(rawStmt, entity);
                return rawStmt.executeAndGetLastInsertRowId();
            } else {
                bindValues(stmt, entity);
                return stmt.executeInsert();
            }
        }
    }

    /**
     * Update the key after every insert ad attach
     * @param entity entity
     * @param rowId rowid
     * @param lock lock
     */
    protected void updateKeyAfterInsertAndAttach(T entity, long rowId, boolean lock) {
        if (rowId != -1) {
            K key = updateKeyAfterInsert(entity, rowId);
            attachEntity(key, entity, lock);
        } else {
            DaoLog.w("Could not insert row (executeInsert returned -1)");
        }
    }

    /**
     * "Saves" an entity to the database: depending on the existence of the key property, it will be inserted
     * (key is null) or updated (key is not null).
     * <p>
     * This is similar to {@link #insertOrReplace(Object)}, but may be more efficient, because if a key is present,
     * it does not have to query if that key already exists.
     *
     * @param entity entity to save
     */
    public void save(T entity) {
        if (hasKey(entity)) {
            update(entity);
        } else {
            insert(entity);
        }
    }

    /**
     * Saves (see {@link #save(Object)}) the given entities in the database using a transaction.
     *
     * @param entities The entities to save.
     */
    public void saveInTx(T... entities) {
        saveInTx(Arrays.asList(entities));
    }

    /**
     * Saves (see {@link #save(Object)}) the given entities in the database using a transaction.
     *
     * @param entities The entities to save.
     */
    public void saveInTx(Iterable<T> entities) {
        int updateCount = 0;
        int insertCount = 0;
        for (T entity : entities) {
            if (hasKey(entity)) {
                updateCount++;
            } else {
                insertCount++;
            }
        }
        if (updateCount > 0 && insertCount > 0) {
            List<T> toUpdate = new ArrayList<>(updateCount);
            List<T> toInsert = new ArrayList<>(insertCount);
            for (T entity : entities) {
                if (hasKey(entity)) {
                    toUpdate.add(entity);
                } else {
                    toInsert.add(entity);
                }
            }

            db.beginTransaction();
            try {
                updateInTx(toUpdate);
                insertInTx(toInsert);
                db.setTransactionSuccessful();
            } finally {
                db.endTransaction();
            }
        } else if (insertCount > 0) {
            insertInTx(entities);
        } else if (updateCount > 0) {
            updateInTx(entities);
        }
    }

    /**
     * Reads all available rows from the given cursor and returns a list of entities.
     * @param resultSet cursor to read data
     * @return list of entities
     */
    protected List<T> loadAllFromCursor(ResultSet cursor) {
        int count = cursor.getRowCount();
        if (count == 0) {
            return new ArrayList<T>();
        }
        List<T> list = new ArrayList<T>(count);
        SharedBlock window = null;
        boolean useFastCursor = false;
        if (cursor instanceof SharedResultSet) {
            window = ((SharedResultSet) cursor).getBlock();
            if (window != null) { // E.g. Robolectric has no Window at this point
                if (window.getRowCount() == count) {
                    cursor = new FastCursor(window);
                    useFastCursor = true;
                } else {
                    DaoLog.d("Window vs. result size: " + window.getRowCount() + "/" + count);
                }
            }
        }

        if (cursor.goToFirstRow()) {
            if (identityScope != null) {
                identityScope.lock();
                identityScope.reserveRoom(count);
            }

            try {
                if (!useFastCursor && window != null && identityScope != null) {
                    loadAllUnlockOnWindowBounds(cursor, window, list);
                } else {
                    do {
                        list.add(loadCurrent(cursor, 0, false));
                    } while (cursor.goToNextRow());
                }
            } finally {
                if (identityScope != null) {
                    identityScope.unlock();
                }
            }
        }
        return list;
    }

    private void loadAllUnlockOnWindowBounds(ResultSet cursor, SharedBlock window, List<T> list) {
        int windowEnd = window.getStartRowIndex() + window.getRowCount();
        for (int row = 0; ; row++) {
            list.add(loadCurrent(cursor, 0, false));
            row++;
            if (row >= windowEnd) {
                window = moveToNextUnlocked(cursor);
                if (window == null) {
                    break;
                }
                windowEnd = window.getStartRowIndex() + window.getRowCount();
            } else {
                if (!cursor.goToNextRow()) {
                    break;
                }
            }
        }
    }

    /**
     * Unlock identityScope during cursor.moveToNext() when it is about to fill the window (needs a db connection):
     * We should not hold the lock while trying to acquire a db connection to avoid deadlocks.
     * @param cursor cursor to be iterated
     * @return Shared block
     */
    private SharedBlock moveToNextUnlocked(ResultSet cursor) {
        identityScope.unlock();
        try {
            if (cursor.goToNextRow()) {
                return ((SharedResultSet) cursor).getBlock();
            } else {
                return null;
            }
        } finally {
            identityScope.lock();
        }
    }

    /**
     * Internal use only. Considers identity scope.
     * @param cursor cursor to be checked
     * @param offset offset
     * @param lock whether db is locked
     * @return generic entity
     */
    protected final T loadCurrent(ResultSet cursor, int offset, boolean lock) {
        if (identityScopeLong != null) {
            if (offset != 0) {
                // Occurs with deep loads (left outer joins)
                if (cursor.isColumnNull((pkOrdinal + offset))) {
                    return null;
                }
            }

            long key = cursor.getLong(pkOrdinal + offset);
            T entity = lock ? identityScopeLong.get2(key) : identityScopeLong.get2NoLock(key);
            if (entity != null) {
                return entity;
            } else {
                entity = readEntity(cursor, offset);
                attachEntity(entity);
                if (lock) {
                    identityScopeLong.put2(key, entity);
                } else {
                    identityScopeLong.put2NoLock(key, entity);
                }
                return entity;
            }
        } else if (identityScope != null) {
            K key = readKey(cursor, offset);
            if (offset != 0 && key == null) {
                // Occurs with deep loads (left outer joins)
                return null;
            }
            T entity = lock ? identityScope.get(key) : identityScope.getNoLock(key);
            if (entity != null) {
                return entity;
            } else {
                entity = readEntity(cursor, offset);
                attachEntity(key, entity, lock);
                return entity;
            }
        } else {
            // Check offset, assume a value !=0 indicating a potential outer join, so check PK
            if (offset != 0) {
                K key = readKey(cursor, offset);
                if (key == null) {
                    // Occurs with deep loads (left outer joins)
                    return null;
                }
            }
            T entity = readEntity(cursor, offset);
            attachEntity(entity);
            return entity;
        }
    }

    /**
     * Internal use only. Considers identity scope.
     * @param dao AbstractDao
     * @param cursor cursor to be iterated
     * @param offset offset
     * @return generic dao result
     */
    protected final <O> O loadCurrentOther(AbstractDao<O, ?> dao, ResultSet cursor, int offset) {
        return dao.loadCurrent(cursor, offset, true);
    }

    /**
     * A raw-style query where you can pass any WHERE clause and arguments.
     * @param where where condition
     * @param selectionArg where arguments
     * @return list of entries
     */
    public List<T> queryRaw(String where, String... selectionArg) {
        ResultSet cursor = db.rawQuery(statements.getSelectAll() + where, selectionArg);
        return loadAllAndCloseCursor(cursor);
    }

    /**
     * Creates a repeatable {@link Query} object based on the given raw SQL where you can pass any WHERE clause and
     * arguments.
     * @param where where condition
     * @param selectionArg where arguments
     * @return repeatable object based on given sql statement
     */
    public Query<T> queryRawCreate(String where, Object... selectionArg) {
        List<Object> argList = Arrays.asList(selectionArg);
        return queryRawCreateListArgs(where, argList);
    }

    /**
     * Creates a repeatable {@link Query} object based on the given raw SQL where you can pass any WHERE clause and
     * arguments.
     * @param where where condition
     * @param selectionArg where arguments
     * @return repeatable object based on given sql statement
     */
    public Query<T> queryRawCreateListArgs(String where, Collection<Object> selectionArg) {
        return Query.internalCreate(this, statements.getSelectAll() + where, selectionArg.toArray());
    }

    /**
     * Delete all entries from a table
     */
    public void deleteAll() {
        db.execSQL("DELETE FROM '" + config.tablename + "'");
        if (identityScope != null) {
            identityScope.clear();
        }
    }

    /**
     * Deletes the given entity from the database. Currently, only single value PK entities are supported.
     * @param entity entity to be deleted
     */
    public void delete(T entity) {
        assertSinglePk();
        K key = getKeyVerified(entity);
        deleteByKey(key);
    }

    /**
     * Deletes an entity with the given PK from the database. Currently, only single value PK entities are supported.
     * @param key entity key to be deleted
     */
    public void deleteByKey(K key) {
        assertSinglePk();
        DatabaseStatement stmt = statements.getDeleteStatement();
        if (db.isDbLockedByCurrentThread()) {
            synchronized (stmt) {
                deleteByKeyInsideSynchronized(key, stmt);
            }
        } else {
            // Do TX to acquire a connection before locking the stmt to avoid deadlocks
            db.beginTransaction();
            try {
                synchronized (stmt) {
                    deleteByKeyInsideSynchronized(key, stmt);
                }
                db.setTransactionSuccessful();
            } finally {
                db.endTransaction();
            }
        }
        if (identityScope != null) {
            identityScope.remove(key);
        }
    }

    private void deleteByKeyInsideSynchronized(K key, DatabaseStatement stmt) {
        if (key instanceof Long) {
            stmt.bindLong(1, (Long) key);
        } else if (key == null) {
            throw new DaoException("Cannot delete entity, key is null");
        } else {
            stmt.bindString(1, key.toString());
        }
        stmt.execute();
    }

    private void deleteInTxInternal(Iterable<T> entities, Iterable<K> keys) {
        assertSinglePk();
        DatabaseStatement stmt = statements.getDeleteStatement();
        List<K> keysToRemoveFromIdentityScope = null;
        db.beginTransaction();
        try {
            synchronized (stmt) {
                if (identityScope != null) {
                    identityScope.lock();
                    keysToRemoveFromIdentityScope = new ArrayList<K>();
                }
                try {
                    if (entities != null) {
                        for (T entity : entities) {
                            K key = getKeyVerified(entity);
                            deleteByKeyInsideSynchronized(key, stmt);
                            if (keysToRemoveFromIdentityScope != null) {
                                keysToRemoveFromIdentityScope.add(key);
                            }
                        }
                    }
                    if (keys != null) {
                        for (K key : keys) {
                            deleteByKeyInsideSynchronized(key, stmt);
                            if (keysToRemoveFromIdentityScope != null) {
                                keysToRemoveFromIdentityScope.add(key);
                            }
                        }
                    }
                } finally {
                    if (identityScope != null) {
                        identityScope.unlock();
                    }
                }
            }
            db.setTransactionSuccessful();
            if (keysToRemoveFromIdentityScope != null && identityScope != null) {
                identityScope.remove(keysToRemoveFromIdentityScope);
            }
        } finally {
            db.endTransaction();
        }
    }

    /**
     * Deletes the given entities in the database using a transaction.
     *
     * @param entities The entities to delete.
     */
    public void deleteInTx(Iterable<T> entities) {
        deleteInTxInternal(entities, null);
    }

    /**
     * Deletes the given entities in the database using a transaction.
     *
     * @param entities The entities to delete.
     */
    public void deleteInTx(T... entities) {
        deleteInTxInternal(Arrays.asList(entities), null);
    }

    /**
     * Deletes all entities with the given keys in the database using a transaction.
     *
     * @param keys Keys of the entities to delete.
     */
    public void deleteByKeyInTx(Iterable<K> keys) {
        deleteInTxInternal(null, keys);
    }

    /**
     * Deletes all entities with the given keys in the database using a transaction.
     *
     * @param keys Keys of the entities to delete.
     */
    public void deleteByKeyInTx(K... keys) {
        deleteInTxInternal(null, Arrays.asList(keys));
    }

    /**
     * Resets all locally changed properties of the entity by reloading the values from the database.
     * @param entity entity for which properties to be reset
     */
    public void refresh(T entity) {
        assertSinglePk();
        K key = getKeyVerified(entity);
        String[] keyArray = new String[] {key.toString()};
        String sql = statements.getSelectByKey();
        ResultSet cursor = db.rawQuery(statements.getSelectByKey(), keyArray);
        try {
            boolean available = cursor.goToFirstRow();
            if (!available) {
                throw new DaoException(
                    "Entity does not exist in the database anymore: " + entity.getClass() + " with key " + key);
            } else if (!cursor.isAtLastRow()) {
                throw new DaoException("Expected unique result, but count was " + cursor.getRowCount());
            }
            readEntity(cursor, entity, 0);
            attachEntity(key, entity, true);
        } finally {
            cursor.close();
        }
    }

    /**
     * Update the datatbase for the entity
     * @param entity entity to be updated
     */
    public void update(T entity) {
        assertSinglePk();
        DatabaseStatement stmt = statements.getUpdateStatement();
        if (db.isDbLockedByCurrentThread()) {
            synchronized (stmt) {
                if (isStandardSQLite) {
                    updateInsideSynchronized(entity, (Statement) stmt.getRawStatement(), true);
                } else {
                    updateInsideSynchronized(entity, stmt, true);
                }
            }
        } else {
            // Do TX to acquire a connection before locking the stmt to avoid deadlocks
            db.beginTransaction();
            try {
                synchronized (stmt) {
                    updateInsideSynchronized(entity, stmt, true);
                }
                db.setTransactionSuccessful();
            } finally {
                db.endTransaction();
            }
        }
    }

    /**
     * Builds a query
     * @return query builder
     */
    public QueryBuilder<T> queryBuilder() {
        return QueryBuilder.internalCreate(this);
    }

    /**
     * Update inside the syncronized flow
     * @param entity entity
     * @param stmt statement
     * @param lock lock
     */
    protected void updateInsideSynchronized(T entity, DatabaseStatement stmt, boolean lock) {
        // To do? Check if it's worth not to bind PKs here (performance).
        bindValues(stmt, entity);
        int index = config.allColumns.length + 1;
        K key = getKey(entity);
        if (key instanceof Long) {
            stmt.bindLong(index, (Long) key);
        } else if (key == null) {
            throw new DaoException("Cannot update entity without key - was it inserted before?");
        } else {
            stmt.bindString(index, key.toString());
        }
        stmt.execute();
        attachEntity(key, entity, lock);
    }

    /**
     * Updtae inside the synchronized block
     * @param entity entity
     * @param stmt statement
     * @param lock lock
     */
    protected void updateInsideSynchronized(T entity, Statement stmt, boolean lock) {
        // To do? Check if it's worth not to bind PKs here (performance).
        bindValues(stmt, entity);
        int index = config.allColumns.length + 1;
        K key = getKey(entity);
        if (key instanceof Long) {
            stmt.setLong(index, (Long) key);
        } else if (key == null) {
            throw new DaoException("Cannot update entity without key - was it inserted before?");
        } else {
            stmt.setString(index, key.toString());
        }
        stmt.execute();
        attachEntity(key, entity, lock);
    }

    /**
     * Attaches the entity to the identity scope. Calls attachEntity(T entity).
     *
     * @param key Needed only for identity scope, pass null if there's none.
     * @param entity The entitiy to attach
     * @param lock if the database is locked
     */
    protected final void attachEntity(K key, T entity, boolean lock) {
        attachEntity(entity);
        if (identityScope != null && key != null) {
            if (lock) {
                identityScope.put(key, entity);
            } else {
                identityScope.putNoLock(key, entity);
            }
        }
    }

    /**
     * Sub classes with relations additionally set the DaoMaster here. Must be called before the entity is attached to
     * the identity scope.
     *
     * @param entity The entitiy to attach
     */
    protected void attachEntity(T entity) {
    }

    /**
     * Updates the given entities in the database using a transaction.
     *
     * @param entities The entities to insert.
     */
    public void updateInTx(Iterable<T> entities) {
        DatabaseStatement stmt = statements.getUpdateStatement();
        db.beginTransaction();
        // txEx: just to preserve original exception in case another exceptions is thrown in endTransaction()
        RuntimeException txEx = null;
        try {
            synchronized (stmt) {
                if (identityScope != null) {
                    identityScope.lock();
                }
                try {
                    if (isStandardSQLite) {
                        Statement rawStmt = (Statement) stmt.getRawStatement();
                        for (T entity : entities) {
                            updateInsideSynchronized(entity, rawStmt, false);
                        }
                    } else {
                        for (T entity : entities) {
                            updateInsideSynchronized(entity, stmt, false);
                        }
                    }
                } finally {
                    if (identityScope != null) {
                        identityScope.unlock();
                    }
                }
            }
            db.setTransactionSuccessful();
        } catch (RuntimeException e) {
            txEx = e;
        } finally {
            try {
                db.endTransaction();
            } catch (RuntimeException e) {
                if (txEx != null) {
                    DaoLog.e("Could not end transaction (rethrowing initial exception)", e);
                }
            }
        }
        if (txEx != null) {
            throw txEx;
        }
    }

    /**
     * Updates the given entities in the database using a transaction.
     *
     * @param entities The entities to update.
     */
    public void updateInTx(T... entities) {
        updateInTx(Arrays.asList(entities));
    }

    /**
     * Assert Single Primary key
     */
    protected void assertSinglePk() {
        if (config.pkColumns.length != 1) {
            throw new DaoException(this + " (" + config.tablename + ") does not have a single-column primary key");
        }
    }

    /**
     * get the statment count
     * @return count
     */
    public long count() {
        return statements.getCountStatement().simpleQueryForLong();
    }

    /**
     * See {@link #getKey(Object)}, but guarantees that the returned key is never null (throws if null).
     * @param entity entity for which key to be verified
     * @return key verified
     */
    protected K getKeyVerified(T entity) {
        K key = getKey(entity);
        if (key == null) {
            if (entity == null) {
                throw new NullPointerException("Entity may not be null");
            } else {
                throw new DaoException("Entity has no key");
            }
        } else {
            return key;
        }
    }

    /**
     * The returned RxDao is a special DAO that let's you interact with Rx Observables without any Scheduler set
     * for subscribeOn.
     * @return RXDao
     */
    @Experimental
    public RxDao<T, K> rxPlain() {
        if (rxDaoPlain == null) {
            rxDaoPlain = new RxDao<>(this);
        }
        return rxDaoPlain;
    }

    /**
     * The returned RxDao is a special DAO that let's you interact with Rx Observables using RX's IO scheduler for
     * subscribeOn.
     * @return RxDao
     */
    @Experimental
    public RxDao<T, K> rx() {
        if (rxDao == null) {
            rxDao = new RxDao<>(this, Schedulers.io());
        }
        return rxDao;
    }

    /**
     * Gets the SQLiteDatabase for custom database access. Not needed for greenDAO entities.
     * @return SQLite Database
     */
    public Database getDatabase() {
        return db;
    }

    /**
     * Reads the values from the current position of the given cursor and returns a new entity.
     * @param cursor cursor to read from
     * @param offset offset
     * @return new entity
     */
    protected abstract T readEntity(ResultSet cursor, int offset);

    /**
     * Reads the key from the current position of the given cursor, or returns null if there's no single-value key.
     * @param cursor cursor to read from
     * @param offset offset
     * @return single value key
     */
    protected abstract K readKey(ResultSet cursor, int offset);

    /**
     * Reads the values from the current position of the given cursor into an existing entity.
     * @param cursor cursor to read from
     * @param entity existing entity
     * @param offset offset
     */
    protected abstract void readEntity(ResultSet cursor, T entity, int offset);

    /**
     * Binds the entity's values to the statement. Make sure to synchronize the statement outside of the method.
     * @param stmt database statement
     * @param entity entity values to bind
     */
    protected abstract void bindValues(DatabaseStatement stmt, T entity);

    /**
     * Binds the entity's values to the statement. Make sure to synchronize the enclosing DatabaseStatement outside
     * of the method.
     * @param stmt statement
     * @param entity entity values to bind
     */
    protected abstract void bindValues(Statement stmt, T entity);

    /**
     * Updates the entity's key if possible (only for Long PKs currently). This method must always return the entity's
     * key regardless of whether the key existed before or not.
     * @param entity entity to be updated
     * @param rowId rowid to be updated
     * @return entity key
     */
    protected abstract K updateKeyAfterInsert(T entity, long rowId);

    /**
     * Returns the value of the primary key, if the entity has a single primary key, or, if not, null. Returns null if
     * entity is null.
     * @param entity entity for which key is required
     * @return value of primary key
     */
    protected abstract K getKey(T entity);

    /**
     * Returns true if the entity is not null, and has a non-null key, which is also != 0.
     * entity is null.
     * @param entity entity for which need to check key
     * @return check if key is there
     */
    protected abstract boolean hasKey(T entity);

    /**
     * Returns true if the Entity class can be updated, e.g. for setting the PK after insert
     * @return if the entity class can be updated
     */
    protected abstract boolean isEntityUpdateable();

}
